// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analysis_server/src/collections.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element2.dart' as engine;
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';

/// A computer for [CompilationUnit] outline.
class DartUnitOutlineComputer {
  final ResolvedUnitResult resolvedUnit;
  final bool withBasicFlutter;

  DartUnitOutlineComputer(this.resolvedUnit, {this.withBasicFlutter = false});

  /// Returns the computed outline, not `null`.
  Outline compute() {
    var unit = resolvedUnit.unit;
    var unitContents = <Outline>[];
    for (var unitMember in unit.declarations) {
      if (unitMember is ClassDeclaration) {
        unitContents.add(
          _newClassOutline(unitMember, _outlinesForMembers(unitMember.members)),
        );
      } else if (unitMember is MixinDeclaration) {
        unitContents.add(
          _newMixinOutline(unitMember, _outlinesForMembers(unitMember.members)),
        );
      } else if (unitMember is EnumDeclaration) {
        unitContents.add(
          _newEnumOutline(unitMember, [
            for (var constant in unitMember.constants)
              _newEnumConstant(constant),
            ..._outlinesForMembers(unitMember.members),
          ]),
        );
      } else if (unitMember is ExtensionDeclaration) {
        unitContents.add(
          _newExtensionOutline(
            unitMember,
            _outlinesForMembers(unitMember.members),
          ),
        );
      } else if (unitMember is ExtensionTypeDeclaration) {
        unitContents.add(
          _newExtensionTypeOutline(
            unitMember,
            _outlinesForMembers(unitMember.members),
          ),
        );
      } else if (unitMember is TopLevelVariableDeclaration) {
        var fieldDeclaration = unitMember;
        var fields = fieldDeclaration.variables;
        var fieldType = fields.type;
        var fieldTypeName = _safeToSource(fieldType);
        for (var field in fields.variables) {
          unitContents.add(
            _newVariableOutline(
              fieldTypeName,
              ElementKind.TOP_LEVEL_VARIABLE,
              field,
              false,
            ),
          );
        }
      } else if (unitMember is FunctionDeclaration) {
        var functionDeclaration = unitMember;
        unitContents.add(_newFunctionOutline(functionDeclaration, true));
      } else if (unitMember is ClassTypeAlias) {
        var alias = unitMember;
        unitContents.add(_newClassTypeAlias(alias));
      } else if (unitMember is FunctionTypeAlias) {
        var alias = unitMember;
        unitContents.add(_newFunctionTypeAliasOutline(alias));
      } else if (unitMember is GenericTypeAlias) {
        var alias = unitMember;
        unitContents.add(_newGenericTypeAliasOutline(alias));
      }
    }
    var unitOutline = _newUnitOutline(unitContents);
    return unitOutline;
  }

  List<Outline> _addFunctionBodyOutlines(FunctionBody body) {
    var contents = <Outline>[];
    body.accept(_FunctionBodyOutlinesVisitor(this, contents));
    return contents;
  }

  Location _getLocationNode(AstNode node) {
    var offset = node.offset;
    var length = node.length;
    return _getLocationOffsetLength(offset, length);
  }

  Location _getLocationOffsetLength(int offset, int length) {
    var path = resolvedUnit.path;
    var startLocation = resolvedUnit.lineInfo.getLocation(offset);
    var startLine = startLocation.lineNumber;
    var startColumn = startLocation.columnNumber;
    var endLocation = resolvedUnit.lineInfo.getLocation(offset + length);
    var endLine = endLocation.lineNumber;
    var endColumn = endLocation.columnNumber;
    return Location(
      path,
      offset,
      length,
      startLine,
      startColumn,
      endLine: endLine,
      endColumn: endColumn,
    );
  }

  Location _getLocationToken(Token token) {
    return _getLocationOffsetLength(token.offset, token.length);
  }

  Outline _newClassOutline(ClassDeclaration node, List<Outline> classContents) {
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.CLASS,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
        isAbstract: node.abstractKeyword != null,
      ),
      location: _getLocationToken(nameToken),
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element, classContents);
  }

  Outline _newClassTypeAlias(ClassTypeAlias node) {
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.CLASS_TYPE_ALIAS,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
        isAbstract: node.abstractKeyword != null,
      ),
      location: _getLocationToken(nameToken),
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element);
  }

  Outline _newConstructorOutline(ConstructorDeclaration constructor) {
    var returnType = constructor.returnType;
    var name = returnType.name;
    var offset = returnType.offset;
    var length = returnType.length;
    var constructorNameToken = constructor.name;
    var isPrivate = false;
    if (constructorNameToken != null) {
      var constructorName = constructorNameToken.lexeme;
      isPrivate = Identifier.isPrivateName(constructorName);
      name += '.$constructorName';
      offset = constructorNameToken.offset;
      length = constructorNameToken.length;
    }
    var parameters = constructor.parameters;
    var parametersStr = _safeToSource(parameters);
    var element = Element(
      ElementKind.CONSTRUCTOR,
      name,
      Element.makeFlags(
        isPrivate: isPrivate,
        isDeprecated: _hasDeprecated(constructor.metadata),
      ),
      location: _getLocationOffsetLength(offset, length),
      parameters: parametersStr,
    );
    var contents = _addFunctionBodyOutlines(constructor.body);
    return _nodeOutline(constructor, element, contents);
  }

  Outline _newEnumConstant(EnumConstantDeclaration node) {
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.ENUM_CONSTANT,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: _getLocationToken(nameToken),
    );
    return _nodeOutline(node, element);
  }

  Outline _newEnumOutline(EnumDeclaration node, List<Outline> children) {
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.ENUM,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: _getLocationToken(nameToken),
    );
    return _nodeOutline(node, element, children);
  }

  Outline _newExtensionOutline(
    ExtensionDeclaration node,
    List<Outline> extensionContents,
  ) {
    var nameToken = node.name;
    var name = nameToken?.lexeme ?? '';

    Location? location;
    if (nameToken != null) {
      location = _getLocationToken(nameToken);
    } else if (node.onClause case var onClause?) {
      location = _getLocationNode(onClause.extendedType);
    }

    var element = Element(
      ElementKind.EXTENSION,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: location,
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element, extensionContents);
  }

  Outline _newExtensionTypeOutline(
    ExtensionTypeDeclaration node,
    List<Outline> extensionContents,
  ) {
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.EXTENSION_TYPE,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: _getLocationToken(nameToken),
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element, extensionContents);
  }

  Outline _newFunctionOutline(FunctionDeclaration function, bool isStatic) {
    var returnType = function.returnType;
    var nameToken = function.name;
    var name = nameToken.lexeme;
    var functionExpression = function.functionExpression;
    var parameters = functionExpression.parameters;
    ElementKind kind;
    if (function.isGetter) {
      kind = ElementKind.GETTER;
    } else if (function.isSetter) {
      kind = ElementKind.SETTER;
    } else {
      kind = ElementKind.FUNCTION;
    }
    var parametersStr = _safeToSource(parameters);
    var returnTypeStr = _safeToSource(returnType);
    var element = Element(
      kind,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(function.metadata),
        isStatic: isStatic,
      ),
      location: _getLocationToken(nameToken),
      parameters: parametersStr,
      returnType: returnTypeStr,
      typeParameters: _getTypeParametersStr(functionExpression.typeParameters),
    );
    var contents = _addFunctionBodyOutlines(functionExpression.body);
    return _nodeOutline(function, element, contents);
  }

  Outline _newFunctionTypeAliasOutline(FunctionTypeAlias node) {
    var returnType = node.returnType;
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var parameters = node.parameters;
    var parametersStr = _safeToSource(parameters);
    var returnTypeStr = _safeToSource(returnType);
    var element = Element(
      ElementKind.FUNCTION_TYPE_ALIAS,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: _getLocationToken(nameToken),
      parameters: parametersStr,
      returnType: returnTypeStr,
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element);
  }

  Outline _newGenericTypeAliasOutline(GenericTypeAlias node) {
    var nameToken = node.name;
    var name = nameToken.lexeme;

    var aliasedType = node.type;
    var aliasedFunctionType =
        aliasedType is GenericFunctionType ? aliasedType : null;

    var element = Element(
      aliasedFunctionType != null
          ? ElementKind.FUNCTION_TYPE_ALIAS
          : ElementKind.TYPE_ALIAS,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      aliasedType: _safeToSource(aliasedType),
      location: _getLocationToken(nameToken),
      parameters:
          aliasedFunctionType != null
              ? _safeToSource(aliasedFunctionType.parameters)
              : null,
      returnType:
          aliasedFunctionType != null
              ? _safeToSource(aliasedFunctionType.returnType)
              : null,
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );

    return _nodeOutline(node, element);
  }

  Outline _newMethodOutline(MethodDeclaration method) {
    var returnType = method.returnType;
    var nameToken = method.name;
    var name = nameToken.lexeme;
    var parameters = method.parameters;
    ElementKind kind;
    if (method.isGetter) {
      kind = ElementKind.GETTER;
    } else if (method.isSetter) {
      kind = ElementKind.SETTER;
    } else {
      kind = ElementKind.METHOD;
    }
    var parametersStr = parameters?.toSource();
    var returnTypeStr = _safeToSource(returnType);
    var element = Element(
      kind,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(method.metadata),
        isAbstract: method.isAbstract,
        isStatic: method.isStatic,
      ),
      location: _getLocationToken(nameToken),
      parameters: parametersStr,
      returnType: returnTypeStr,
      typeParameters: _getTypeParametersStr(method.typeParameters),
    );
    var contents = _addFunctionBodyOutlines(method.body);
    return _nodeOutline(method, element, contents);
  }

  Outline _newMixinOutline(MixinDeclaration node, List<Outline> mixinContents) {
    node.firstTokenAfterCommentAndMetadata;
    var nameToken = node.name;
    var name = nameToken.lexeme;
    var element = Element(
      ElementKind.MIXIN,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(node.metadata),
      ),
      location: _getLocationToken(nameToken),
      typeParameters: _getTypeParametersStr(node.typeParameters),
    );
    return _nodeOutline(node, element, mixinContents);
  }

  Outline _newUnitOutline(List<Outline> unitContents) {
    var unit = resolvedUnit.unit;
    var element = Element(
      ElementKind.COMPILATION_UNIT,
      '<unit>',
      Element.makeFlags(),
      location: _getLocationNode(unit),
    );
    return _nodeOutline(unit, element, unitContents);
  }

  Outline _newVariableOutline(
    String typeName,
    ElementKind kind,
    VariableDeclaration variable,
    bool isStatic,
  ) {
    var nameToken = variable.name;
    var name = nameToken.lexeme;
    var element = Element(
      kind,
      name,
      Element.makeFlags(
        isPrivate: Identifier.isPrivateName(name),
        isDeprecated: _hasDeprecated(variable.metadata),
        isStatic: isStatic,
        isConst: variable.isConst,
        isFinal: variable.isFinal,
      ),
      location: _getLocationToken(nameToken),
      returnType: typeName,
    );
    return _nodeOutline(variable, element);
  }

  Outline _nodeOutline(
    AstNode node,
    Element element, [
    List<Outline>? children,
  ]) {
    var offset = node.offset;
    var end = node.end;
    if (node is VariableDeclaration) {
      var parent = node.parent;
      var grandParent = parent?.parent;
      if (grandParent != null &&
          parent is VariableDeclarationList &&
          parent.variables.isNotEmpty) {
        if (parent.variables[0] == node) {
          offset = grandParent.offset;
        }
        if (parent.variables.last == node) {
          end = grandParent.end;
        }
      }
    }

    var codeOffset = node.offset;
    if (node is AnnotatedNode) {
      codeOffset = node.firstTokenAfterCommentAndMetadata.offset;
    }

    var length = end - offset;
    var codeLength = node.end - codeOffset;
    return Outline(
      element,
      offset,
      length,
      codeOffset,
      codeLength,
      children: nullIfEmpty(children),
    );
  }

  List<Outline> _outlinesForMembers(List<ClassMember> members) {
    var memberOutlines = <Outline>[];
    for (var classMember in members) {
      if (classMember is ConstructorDeclaration) {
        var constructorDeclaration = classMember;
        memberOutlines.add(_newConstructorOutline(constructorDeclaration));
      }
      if (classMember is FieldDeclaration) {
        var fieldDeclaration = classMember;
        var fields = fieldDeclaration.fields;
        var fieldType = fields.type;
        var fieldTypeName = _safeToSource(fieldType);
        for (var field in fields.variables) {
          memberOutlines.add(
            _newVariableOutline(
              fieldTypeName,
              ElementKind.FIELD,
              field,
              fieldDeclaration.isStatic,
            ),
          );
        }
      }
      if (classMember is MethodDeclaration) {
        var methodDeclaration = classMember;
        memberOutlines.add(_newMethodOutline(methodDeclaration));
      }
    }
    return memberOutlines;
  }

  static String? _getTypeParametersStr(TypeParameterList? parameters) {
    if (parameters == null) {
      return null;
    }
    return parameters.toSource();
  }

  /// Whether the list of [annotations] includes a `deprecated` annotation.
  static bool _hasDeprecated(List<Annotation> annotations) {
    for (var annotation in annotations) {
      if (annotation.elementAnnotation?.isDeprecated ?? false) {
        return true;
      }
    }
    return false;
  }

  static String _safeToSource(AstNode? node) =>
      node == null ? '' : node.toSource();
}

/// A visitor for building local function outlines.
class _FunctionBodyOutlinesVisitor extends RecursiveAstVisitor<void> {
  final DartUnitOutlineComputer outlineComputer;
  final List<Outline> contents;

  _FunctionBodyOutlinesVisitor(this.outlineComputer, this.contents);

  /// Return `true` if the given [element] is the method 'group' defined in the
  /// test package.
  bool isGroup(engine.ExecutableElement2? element) {
    if (element != null && element.metadata2.hasIsTestGroup) {
      return true;
    }
    return element is engine.TopLevelFunctionElement &&
        element.name3 == 'group' &&
        _isInsideTestPackage(element);
  }

  /// Return `true` if the given [element] is the method 'test' defined in the
  /// test package.
  bool isTest(engine.ExecutableElement2? element) {
    if (element != null && element.metadata2.hasIsTest) {
      return true;
    }
    return element is engine.TopLevelFunctionElement &&
        element.name3 == 'test' &&
        _isInsideTestPackage(element);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    contents.add(outlineComputer._newFunctionOutline(node, false));
  }

  @override
  void visitInstanceCreationExpression(InstanceCreationExpression node) {
    if (outlineComputer.withBasicFlutter && node.isWidgetCreation) {
      var children = <Outline>[];
      node.argumentList.accept(
        _FunctionBodyOutlinesVisitor(outlineComputer, children),
      );

      // The method `getWidgetPresentationText` should not return `null` when
      // `isWidgetCreation` returns `true`.
      var text = node.widgetPresentationText ?? '<unknown>';
      var element = Element(
        ElementKind.CONSTRUCTOR_INVOCATION,
        text,
        0,
        location: outlineComputer._getLocationOffsetLength(node.offset, 0),
      );

      contents.add(
        Outline(
          element,
          node.offset,
          node.length,
          node.offset,
          node.length,
          children: nullIfEmpty(children),
        ),
      );
    } else {
      super.visitInstanceCreationExpression(node);
    }
  }

  @override
  void visitMethodInvocation(MethodInvocation node) {
    var nameNode = node.methodName;

    var nameElement = nameNode.element;
    if (nameElement is! engine.ExecutableElement2) {
      return;
    }

    String extractString(NodeList<Expression>? arguments) {
      if (arguments != null && arguments.isNotEmpty) {
        var argument = arguments[0];
        if (argument is StringLiteral) {
          var value = argument.stringValue;
          if (value != null) {
            return value;
          }
        }
        return argument.toSource();
      }
      return 'unnamed';
    }

    void addOutlineNode(ElementKind kind, [List<Outline>? children]) {
      var executableName = nameNode.name;
      var description = extractString(node.argumentList.arguments);
      var name = '$executableName("$description")';
      var element = Element(
        kind,
        name,
        0,
        location: outlineComputer._getLocationNode(nameNode),
      );
      contents.add(
        Outline(
          element,
          node.offset,
          node.length,
          node.offset,
          node.length,
          children: nullIfEmpty(children),
        ),
      );
    }

    if (isGroup(nameElement)) {
      var groupContents = <Outline>[];
      node.argumentList.accept(
        _FunctionBodyOutlinesVisitor(outlineComputer, groupContents),
      );
      addOutlineNode(ElementKind.UNIT_TEST_GROUP, groupContents);
    } else if (isTest(nameElement)) {
      addOutlineNode(ElementKind.UNIT_TEST_TEST);
    } else {
      super.visitMethodInvocation(node);
    }
  }

  /// Return `true` if the given [element] is a top-level member of the test
  /// package.
  bool _isInsideTestPackage(engine.TopLevelFunctionElement element) {
    var parent = element.library2;
    return parent.firstFragment.source.fullName.endsWith('test.dart');
  }
}
